﻿using System;
using System.ComponentModel;
using System.Runtime.Serialization;

namespace SharpGL.SceneGraph.Effects
{
    /// <summary>
    /// An effect is something that can be applied to a scene element which 
    /// then changes everything in the tree below it. It can be pushed, to apply it
    /// and popped, to restore OpenGL back to the state without the effect.
    /// </summary>
    [TypeConverter(typeof(System.ComponentModel.ExpandableObjectConverter))]
    [DataContract(/*IsReference = true, */Name = "ContextualEffect", Namespace = "SharpGL.SceneGraph")]
    public abstract class ContextualEffect : Effect, IHasOpenGLContext, INotifyContextChanged
    {
        #region Private Consts

        private static readonly OpenGL defaultCurrentOpenGLContext = null;

        #endregion Private Consts

        #region Private Fields

        /// <summary>
        /// Current OpenGL context
        /// </summary>
        [IgnoreDataMember()]
        private OpenGL currentOpenGLContext;

        #endregion Private Fields

        #region Protected Methods

        /// <summary>
        /// Call this function to do the ContextChanged event.
        /// </summary>
        protected void NotifyContextChanged(ContextChangeAction action)
        {
            var handler = ContextChanged;
            if (handler != null)
                handler(this, new ContextChangeEventArgs(action));
        }

        /// <summary>
        /// Called when creating in the OpenGL instance.
        /// </summary>
        /// <param name="gl">The OpenGL instance.</param>
        protected abstract void OnCreateInContext(OpenGL gl);

        protected override void OnDeserializing()
        {
            base.OnDeserializing();

            currentOpenGLContext = defaultCurrentOpenGLContext;
        }

        /// <summary>
        /// Called when destroy in the OpenGL instance.
        /// </summary>
        protected abstract void OnDestroyInContext(OpenGL gl);

        #endregion

        #region Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="ContextualEffect"/> class.
        /// </summary>
        public ContextualEffect()
        {
            currentOpenGLContext = defaultCurrentOpenGLContext;
        }

        #endregion Constructors

        #region Public Methods

        /// <summary>
        /// Create in the context of the supplied OpenGL instance.
        /// </summary>
        /// <param name="gl">The OpenGL instance.</param>
        public virtual void CreateInContext(OpenGL gl)
        {
            if (!OpenGL.Equals(gl, currentOpenGLContext))
            {
                if (OpenGL.IsValidContext(currentOpenGLContext))
                    throw new Exception("Cannot create '" +
                                        Name +
                                        "' in context '" +
                                        (OpenGL.IsValidContext(gl) ? gl.Renderer : "Null") +
                                        "' - it is already created in '" +
                                        (OpenGL.IsValidContext(currentOpenGLContext) ? currentOpenGLContext.Renderer : "Null") +
                                        "' OpenGL context.");


                if (OpenGL.IsValidContext(gl))
                {
                    currentOpenGLContext = gl;

                    // Call on create in OpenGL context function.
                    OnCreateInContext(gl);

                    NotifyContextChanged(ContextChangeAction.CreateInContext);
                }
            }
        }

        /// <summary>
        /// Destroy in the current OpenGL instance.
        /// </summary>
        public virtual void DestroyInContext()
        {
            if (OpenGL.IsValidContext(currentOpenGLContext))
            {
                NotifyContextChanged(ContextChangeAction.DestroyInContext);

                // Call on destroy in OpenGL context function.
                OnDestroyInContext(currentOpenGLContext);

                currentOpenGLContext = null;
            }
        }

        /// <summary>
        /// Pushes the effect onto the specified parent element.
        /// </summary>
        /// <param name="gl">The OpenGL instance.</param>
        /// <param name="parentElement">The parent element.</param>
        public override void Push(OpenGL gl, SceneElement parentElement)
        {
            if (!OpenGL.Equals(currentOpenGLContext, gl))
                throw new Exception("Can't push '" +
                                    Name +
                                    "' in context '" +
                                    (OpenGL.IsValidContext(gl) ? gl.Renderer : "Null") +
                                    "' - it is created in '" +
                                    (OpenGL.IsValidContext(currentOpenGLContext) ? currentOpenGLContext.Renderer : "Null") +
                                    "' OpenGL context.");
        }

        /// <summary>
        /// Pops the effect off the specified parent element.
        /// </summary>
        /// <param name="gl">The OpenGL instance.</param>
        /// <param name="parentElement">The parent element.</param>
        public override void Pop(OpenGL gl, SceneElement parentElement)
        {
            if (!OpenGL.Equals(currentOpenGLContext, gl))
                throw new Exception("Can't pop '" +
                                    Name +
                                    "' in context '" +
                                    (OpenGL.IsValidContext(gl) ? gl.Renderer : "Null") +
                                    "' - it is created in '" +
                                    (OpenGL.IsValidContext(currentOpenGLContext) ? currentOpenGLContext.Renderer : "Null") +
                                    "' OpenGL context.");
        }

        #endregion Public Methods

        #region Public Events

        [Description("Called when OpenGL context of effect has been changed."), Category("SharpGL")]
        public event ContextChangeEventHandler ContextChanged;

        #endregion Public Events

        #region Public Properties

        /// <summary>
        /// Current OpenGL context
        /// </summary>
        [Browsable(false)]
        public OpenGL CurrentOpenGLContext
        {
            get { return currentOpenGLContext; }
            protected set { currentOpenGLContext = value;  }
        }

        #endregion Public Properties
    }
}